这个笔记是根据慕课网上一门叫做《Java秒杀系统方案优化 高性能高并发实战》学习整理的笔记。学习应对高并发场景如何设计接口,以及后端架构如何优化等知识,觉得还是学到一些东西的,就分享在这里。第一篇主要是完成用户登陆模块,借此搭建起一个基本的系统。
目标:初步实现用户登录功能.
1. user表结构
1 2 3 4 5 6 7 8 9 10 11 12
| CREATE TABLE `NewTable` ( `id` bigint NOT NULL COMMENT '手机号码' , `nickname` varchar(255) NOT NULL COMMENT '登录名' , `password` varchar(32) NOT NULL COMMENT 'md5(md5(pass+固定salt)+salt)' , `salt` varchar(10) NOT NULL COMMENT '盐值' , `head` varchar(128) NOT NULL COMMENT '头像' , `register_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '注册时间' , `last_login_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '上次登录时间' , `login_count` int(11) NOT NULL DEFAULT 0 COMMENT '登录次数' , PRIMARY KEY (`id`) ) ;
|
两次MD5
:
- 用户端:PASS=MD5(明文+固定salt):防止明文密码在网络传输时被截取
- 服务端:PASS=MD5(用户输入+随机salt):防止数据库被盗
2. 代码逻辑
2.1 前端处理
这里在前端对密码进行了一次md5
加密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <script> function login(){ $("#loginForm").validate({ submitHandler:function(form){ doLogin(); } }); } function doLogin(){ g_showLoading(); var inputPass = $("#password").val(); var salt = g_passsword_salt; var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4); var password = md5(str); $.ajax({ url: "/login/do_login", type: "POST", data:{ mobile:$("#mobile").val(), password: password }, success:function(data){ layer.closeAll(); if(data.code == 0){ layer.msg("成功"); window.location.href="/goods/to_list"; }else{ layer.msg(data.msg); } }, error:function(){ layer.closeAll(); } }); } </script>
|
这里前端的渲染模板用的是thymeleaf
:
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
|
后端如何优雅地处理呢?
2.2 定义一个vo来接收前端数据
1 2 3 4 5
| @Data public class LoginVo { private String mobile; private String password; }
|
2.3 数据校验
我们可以用jsr303来进行校验,而不需要写很多代码来实现。
1 2 3 4 5 6 7 8 9 10
| @Data public class LoginVo { @NotNull @IsMobile private String mobile;
@NotNull @Length(min=32) private String password; }
|
这里需要依赖:
1 2 3 4 5
| <!--jsr303--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
|
对于其中的判断手机号码是否存在,我们需要自己来实现一下这个注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {IsMobileValidator.class }) public @interface IsMobile { boolean required() default true; String message() default "手机号码格式错误";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { }; }
|
这个注解的功能是由IsMobileValidator.class
来完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false; public void initialize(IsMobile constraintAnnotation) { required = constraintAnnotation.required(); }
public boolean isValid(String value, ConstraintValidatorContext context) { if(required) { return ValidatorUtil.isMobile(value); }else { if(StringUtils.isEmpty(value)) { return true; }else { return ValidatorUtil.isMobile(value); } } }
}
|
其中,ValidatorUtil.isMobile(value)
是真正用来验证手机格式的:
1 2 3 4 5 6 7 8 9 10 11 12
| public class ValidatorUtil { private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); public static boolean isMobile(String src) { if(StringUtils.isEmpty(src)) { return false; } Matcher m = mobile_pattern.matcher(src); return m.matches(); } }
|
这样,我们就可以实现对前端传来的参数进行校验了:@Valid LoginVo loginVo
1 2 3 4 5 6
| @RequestMapping("/do_login") @ResponseBody public Result<Boolean> doLogin(@Valid LoginVo loginVo){ userService.login(loginVo); return Result.success(true); }
|
2.4 全局异常
当校验参数时,这个参数时有问题时,我们需要一个全局异常来进行处理,将异常信息以合适的形式传给前端:
GlobalException:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class GlobalException extends RuntimeException{
private static final long serialVersionUID = 1L; private CodeMsg cm; public GlobalException(CodeMsg cm) { super(cm.toString()); this.cm = cm; }
public CodeMsg getCm() { return cm; } }
|
下面就是需要对异常进行拦截和处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @ControllerAdvice @ResponseBody public class GlobalExceptionHandler { @ExceptionHandler(value=Exception.class) public Result<String> exceptionHandler(HttpServletRequest request, Exception e){ e.printStackTrace(); if(e instanceof GlobalException) { GlobalException ex = (GlobalException)e; return Result.error(ex.getCm()); }else if(e instanceof BindException) { BindException ex = (BindException)e; List<ObjectError> errors = ex.getAllErrors(); ObjectError error = errors.get(0); String msg = error.getDefaultMessage(); return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg)); }else { return Result.error(CodeMsg.SERVER_ERROR); } } }
|
2.5 返回结果封装类
我们给前端返回的结果要有一个统一的格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @Data public class Result<T> { private int code; private String msg; private T data;
public static <T> Result<T> success(T data){ return new Result<T>(data); }
public static <T> Result<T> error(CodeMsg codeMsg){ return new Result<T>(codeMsg); } private Result(T data) { this.data = data; } private Result(int code, String msg) { this.code = code; this.msg = msg; } private Result(CodeMsg codeMsg) { if(codeMsg != null) { this.code = codeMsg.getCode(); this.msg = codeMsg.getMsg(); } }
}
|
2.6 异常信息分类
因为会产生各种异常,为了方便出现问题时很快定位到异常的类型,我们需要对异常的类型进行统一的管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Data @NoArgsConstructor @AllArgsConstructor @ToString public class CodeMsg { private int code; private String msg; public static CodeMsg SUCCESS = new CodeMsg(0, "success"); public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常"); public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s"); public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效"); public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空"); public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空"); public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误"); public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在"); public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误");
public CodeMsg fillArgs(Object... args) { int code = this.code; String message = String.format(this.msg, args); return new CodeMsg(code, message); } }
|
2.7 login登录逻辑
手机号码不存在或者密码不匹配,直接抛出全局异常异常,这个异常信息会被拦截,最后处理成相应的统一的格式进行返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public boolean login(LoginVo loginVo) { if (loginVo == null) throw new GlobalException(CodeMsg.SERVER_ERROR); String mobile = loginVo.getMobile(); String password = loginVo.getPassword();
MiaoshaUser user = getById(Long.parseLong(mobile)); if(user == null){ throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST); }
String dbPass = user.getPassword(); String dbSalt = user.getSalt(); if(!MD5Util.formPassToDBPass(password,dbSalt).equals(dbPass)){ throw new GlobalException(CodeMsg.PASSWORD_ERROR); }
return true; }
|
这里需要一个MD5
的工具类,不贴了,但是注意要添加依赖:
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.1</version> </dependency>
|